﻿(*
Simple 3D Rendering ( * )

We will now move on to 3D rendering.
3D rendering is almost as simple as 2D rendering, but with the added difficulty of:
- expressing the viewpoint in 3D
- perspective transformations

We will load from the content pipeline four models, which are files in the .fbx and .x format and which contain a 3D model.
Those models are loaded as instances of the Model class, which has the powerful Draw method that draws the model to the screen.
Drawing a model requires three transformation matrices, called World, View and Projection.
Each one of these matrices represents a crucial aspect of a 3D scene:
- the World matrix represents the local translation, rotation and scale of the object; when an object moves or rotates, its world matrix changes accordingly
- the View matrix represents the observer position and orientation, and is the same for all objects; when the user moves, then the view matrix changes
- the Projection matrix contains the properties of the camera: its aspect ratio, its field of view, etc; if the camera changes (a sniper rifle, for example) then the projection matrix changes

we create these matrices very simply with the aid of the large math library offered by XNA, pass them to the Draw method of our models and are set.
*)
namespace Chapter6

  (*
  Most of our game will be very similar to the asteroid field game.
  *)
  module Simple3D =

    open System
    open System.Collections.Generic
    open Microsoft.Xna.Framework
    open Microsoft.Xna.Framework.Graphics
    open Microsoft.Xna.Framework.Input
    open Chapter5

    type Chapter2.Math.Vector2< [<Measure>] 'u >
      with
        member this.ToXNAVector = Vector2(this.X |> float32, this.Y |> float32)

    type Game3D() as this = 
      inherit Game()
      let graphics = new GraphicsDeviceManager(this)
      do graphics.GraphicsProfile <- GraphicsProfile.HiDef

      (*
      Instead of textures, we now have models (one for each type of object rendered).
      *)
      let mutable police_ship    : Model = null
      let mutable pirate_ship    : Model = null
      let mutable cargo_ship     : Model = null
      let mutable police_station : Model = null

      let mutable zoom = 1.0f
      let mutable center = Vector3.Zero

      (*
      We initialize the view matrix with the Matrix.CreateLookAt method, which takes as input:
      - the camera position
      - the point the camera is looking at
      - the up direction relative to the camera

      Since the view matrix can change from one frame to the next (when the zoom and center fields are modified) then it is declared as mutable.
      *)
      let mutable view = Matrix.CreateLookAt(Vector3.Forward * 30.0f, Vector3.Zero, Vector3.Up)

      (*
      The projection matrix is initialized with the Matrix.CreatePerspectiveFieldOfView method, which takes as input:
      - the field of view (an angle in radians)
      - the aspect ratio of the screen
      - the minimum distance before which we do not see
      - the maximum distance beyond which we do not see
      *)
      let projection = Matrix.CreatePerspectiveFieldOfView(1.5f, 1.6f, 0.1f, 1000.0f)

      (*
      We initialize the state of the simulation:
      *)
      let police_chase = PoliceChase.s0()

      do this.Content.RootDirectory <- "Content"

      (*
      In the load method we load the various starship models and the police station; 
      loading models is exactly the same as loading textures, only type inference is understanding that the full signature is Content.Load<Model> and not Content.Load<Texture>:
      *)  
      override this.LoadContent() = 
        do police_ship    <- this.Content.Load(@"3D\A1")
        do pirate_ship    <- this.Content.Load(@"3D\A2")
        do cargo_ship     <- this.Content.Load(@"3D\A3")
        do police_station <- this.Content.Load(@"3D\station")
        base.LoadContent()

      (*
      Updating the simulation follows the same scheme as the other updates we have encountered in the previous sections.
      We start by updating the simulation with the internal application logic, and then we poll some input devices.
      After reading the user input we rebuild the view matrix, so that the user may move the camera with the arrows and zoom in with A/S.
      *)
      override this.Update gt = 
        do PoliceChase.simulation_step(police_chase)

        let dt = gt.ElapsedGameTime.TotalSeconds |> float32
        if Keyboard.GetState().[Keys.A] = KeyState.Down then
          do zoom <- zoom * 1.01f
        elif Keyboard.GetState().[Keys.S] = KeyState.Down then
          do zoom <- zoom / 1.01f

        if Keyboard.GetState().[Keys.Left] = KeyState.Down then
          do center.X <- center.X + dt * 10.0f / (zoom * zoom)
        if Keyboard.GetState().[Keys.Right] = KeyState.Down then
          do center.X <- center.X - dt * 10.0f / (zoom * zoom)
        if Keyboard.GetState().[Keys.Up] = KeyState.Down then
          do center.Y <- center.Y + dt * 10.0f / (zoom * zoom)
        if Keyboard.GetState().[Keys.Down] = KeyState.Down then
          do center.Y <- center.Y - dt * 10.0f / (zoom * zoom)

        do view <- Matrix.CreateLookAt(Vector3.Forward * 30.0f * zoom + center, center, Vector3.Up)
        base.Update gt

      (*
      The Draw method will invoke the Model.Draw method with the right World, View and Projection for each entity of hte game state.
      *)
      override this.Draw gt = 
        do this.GraphicsDevice.Clear(Color.Black)

        (*
        Since our camera is at 30 units from the center of the action, then some scaling must happen in order for us to see something.
        Our simulation works on a very large scale (the size of the playing field is various millions of km) and so we will normalize our positions.
        The convert_position function takes as input a 2D position, scales it by the field_size and moves it into the [-1..+1]^2 range.
        The resulting position is then scaled by a factor of 20, and adapted to the aspect ratio of the screen:
        *)
        let convert_position (position:Vector2) (z:float32) =
          let field_size = float32(PoliceChase.field_size)
          let position = position / field_size
          let position = position * 2.0f - Vector2.One
          Vector3(Vector2(this.GraphicsDevice.Viewport.AspectRatio,1.0f) * 20.0f * position, z)

        (*
        To draw a model, with a certain position and rotation, we create a world matrix that uses a rotation around the Z axis multiplied by a translation of the converted position.
        The view and projection parameters are the same for all draw calls:
        *)
        let draw_model (model:Model) (z:float32) (position:Vector2) (rotation:float32) =
          let position = convert_position position z
          do model.Draw(Matrix.CreateRotationZ(rotation) *
                        Matrix.CreateTranslation(position),
                        view,
                        projection)

        (*
        To draw a ship we compute its rotation so that the ship looks in the direction it is moving (the atan2 function computes the angle, in radians, that corresponds to a 2D vector)
        and we invoke the draw_model function to the ship position and rotation.
        We also pass a z parameter that allows us to add a vertical displacement so that the ships do not overlap:
        *)
        let draw_ship (model:Model) (z:float32) (ship:PoliceChase.Ship) =
          let rotation = (atan2 ship.Velocity.X ship.Velocity.Y) |> float32
          do draw_model model z (ship.Position.ToXNAVector) rotation

        (*
        Finally, we draw each the police station with the draw_model function, and each ship that has not been destroyed with the draw_ship function:
        *)
        do draw_model police_station 1.0f (police_chase.PoliceStation.Position.ToXNAVector) (float32 gt.TotalGameTime.TotalMinutes * 2.0f)
        if police_chase.Patrol.Integrity > 0.0<_> then
          do police_chase.Patrol |> draw_ship police_ship 1.0f
        if police_chase.Pirate.Integrity > 0.0<_> then
          do police_chase.Pirate |> draw_ship pirate_ship -1.0f
        if police_chase.Cargo.Integrity > 0.0<_> then
          do police_chase.Cargo  |> draw_ship cargo_ship  0.0f

        base.Draw gt
